Capital Annual Growth Rate

import numpy as np
import matplotlib.pyplot as plt
import datetime as dt
import yfinance as yf

plt.style.use("ggplot")

\[ C A G R=\left[\left(\frac{E V}{B V}\right)^{1/n}-1\right]\times 100 \] where: \(E V=\) Ending value
\(B V=\) Beginning value
\(n=\) Number of years

sp500 = yf.download(
    ["^GSPC"],
    start=dt.datetime.today() - dt.timedelta(days=1500),
    end=dt.datetime.today(),
    progress=True,
    actions="inline",
    interval="1d",
)
[*********************100%***********************]  1 of 1 completed

This function is for daily data, because we use 252 number of trading days in a year to calculate \(n\).

def get_cagr(df):
    EV = df["Close"][-1]
    BV = df["Close"][0]
    n = len(df) / 252
    cagr = (EV / BV) ** (1 / n) - 1
    print("CAGR: {:.2f}%".format(cagr * 100))
    return cagr
cagr = get_cagr(sp500)
CAGR: 14.62%
/tmp/ipykernel_4295/3211283570.py:2: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

/tmp/ipykernel_4295/3211283570.py:3: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

Volatility

Simple as you have imagined, volatility is commonly measured by standard deviation. The only thing you need to take heed is to know how to convert to annualized volatility. \[ \text{daily return} \times \sqrt{252}\\ \text{weekly return} \times \sqrt{52}\\ \text{monthly return} \times \sqrt{12} \]

def get_volatility(df, freq):
    """
    The function is for using daily trading data.
    """
    daily_ret = df["Close"].pct_change().std()
    if freq == "daily":
        vol = daily_ret * np.sqrt(252)
    elif freq == "weekly":
        vol = daily_ret * np.sqrt(52)
    elif freq == "monthly":
        vol = daily_ret * np.sqrt(12)
    return vol
get_volatility(sp500, "daily")
0.1664305706560202

Sharpe Ratio and Sortino Ratio

\[ Sh.R = \frac{r_p - r_{f}}{\sigma_p} \]

cisco = yf.download(
    ["CSCO"],
    start=dt.datetime.today() - dt.timedelta(days=3500),
    end=dt.datetime.today(),
    progress=True,
    actions="inline",
    interval="1d",
)
[*********************100%***********************]  1 of 1 completed
def get_sharpe(df, rf):
    sr = (get_cagr(df) - rf) / get_volatility(df, "daily")
    print("Risk-free rate:{}%".format(rf * 100))
    print("Sharpe Ratio: {}".format(sr))
    return sr
sr = get_sharpe(cisco, 0.045)
CAGR: 7.60%
Risk-free rate:4.5%
Sharpe Ratio: 0.12339481243914618
/tmp/ipykernel_4295/3211283570.py:2: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

/tmp/ipykernel_4295/3211283570.py:3: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

\[ So. R = \frac{r_p-r_f}{\sigma_p^*} \]

where \(\sigma_p^*\) only takes account of negative volatility.

def get_sortino(df, rf):
    df["daily_ret"] = df["Close"].pct_change()
    neg_volatility = df["daily_ret"][df["daily_ret"] < 0].std() * np.sqrt(252)
    sor = (get_cagr(df) - rf) / neg_volatility
    print("Risk-free rate:{}%".format(rf * 100))
    print("Sortino Ratio: {}".format(sor))
    return sor


sor = get_sortino(cisco, 0.045)
CAGR: 7.60%
Risk-free rate:4.5%
Sortino Ratio: 0.15548974683152755
/tmp/ipykernel_4295/3211283570.py:2: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

/tmp/ipykernel_4295/3211283570.py:3: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

Max Drawdown and Calmar Ratio

Max drawdown is the percentage counts the maximum drop from peak return in a certain period.

def get_max_dd(df):
    df["daily_ret"] = df["Close"].pct_change()
    df["cum_ret"] = (1 + df["daily_ret"]).cumprod()
    df["cum_trailing_max"] = df["cum_ret"].cummax()
    df["drawdown"] = df["cum_trailing_max"] - df["cum_ret"]
    df["drawdown_pct"] = df["drawdown"] / df["cum_trailing_max"]
    max_dd = df["drawdown_pct"].max()
    print("Max drawdown: {:.4f}%".format(max_dd * 100))
    return max_dd
mdd = get_max_dd(cisco)
Max drawdown: 42.8079%

Calmar ratio is similar to Sharpe ratio, but the risk is replace by maximum drawdown. A hedge fund manager named Terry W. Young invented Calmar ratio, and ‘Calmar’ is an acronym of his company’s name and its newsletter: CALifornia Managed Accounts Reports.

def get_calmar(df):
    calmar = get_cagr(df) / get_max_dd(df)
    print("Calmar ratio: {:.4f}%".format(calmar))
    return calmar
get_calmar(cisco)
CAGR: 7.60%
Max drawdown: 42.8079%
Calmar ratio: 0.1775%
/tmp/ipykernel_4295/3211283570.py:2: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`

/tmp/ipykernel_4295/3211283570.py:3: FutureWarning:

Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
0.17752693121492458